package internals

import (
	"reflect"

	zconst "github.com/Oudwins/zog/zconst"
	"golang.org/x/exp/constraints"
)

// A TestFunc that takes the data as input and returns a boolean indicating if it is valid or not
type BoolTFunc[T any] func(val T, ctx Ctx) bool

// TFunc is the function that tests hold that execute on the data for validation. They use the z.Ctx to add issues if needed
type TFunc[T any] func(val T, ctx Ctx)

// TestOption is the option for a test
type TestOption func(test TestInterface)

func TestFuncFromBool[T any](fn BoolTFunc[T], test *Test[T]) {
	test.Func = func(val T, ctx Ctx) {
		if fn(val, ctx) {
			return
		}

		c := ctx.(*SchemaCtx)
		ctx.AddIssue(c.IssueFromTest(c.Processor.(TestInterface), val))
	}
}

func TestNotFuncFromBool[T any](fn BoolTFunc[T], test *Test[T]) {
	test.Func = func(val T, ctx Ctx) {
		if !fn(val, ctx) {
			return
		}

		c := ctx.(*SchemaCtx)
		ctx.AddIssue(c.IssueFromTest(c.Processor.(TestInterface), val))
	}
}

func NewTestFunc[T any](IssueCode zconst.ZogIssueCode, fn BoolTFunc[T], options ...TestOption) *Test[T] {
	t := &Test[T]{
		IssueCode: IssueCode,
	}
	for _, opt := range options {
		opt(t)
	}
	TestFuncFromBool(fn, t)
	return t
}

type TestInterface interface {
	GetIssueCode() zconst.ZogIssueCode
	GetIssuePath() string
	GetParams() map[string]any
	GetIssueFmtFunc() IssueFmtFunc
	SetIssueCode(code zconst.ZogIssueCode)
	SetIssuePath(path string)
	SetParams(params map[string]any)
	SetIssueFmtFunc(fmter IssueFmtFunc)
}

// Test is a struct that represents an individual validation. For example `z.String().Min(3)` is a test that checks if the string is at least 3 characters long.
type Test[T any] struct {
	// The issue code that will be used to create the issue if the test fails
	IssueCode zconst.ZogIssueCode
	// The path to the issue
	IssuePath string
	// The params for the test which are passed to the issue and can be used for customizing issue messages. For example `z.String().Min(3)` will have a param with the key `min` and the value `3`.
	Params map[string]any
	// The formatter for the issue. You can use this if you want to customize the message formatter for issues generated by this specific test. Not super useful but just here for completeness.
	IssueFmtFunc IssueFmtFunc
	// The function that will be executed when evaluating the test.
	Func TFunc[T]
}

func (t *Test[T]) ZProcess(valPtr T, ctx Ctx) {
	t.Func(valPtr, ctx)
}

func (t *Test[T]) GetIssueCode() zconst.ZogIssueCode {
	return t.IssueCode
}

func (t *Test[T]) GetIssuePath() string {
	return t.IssuePath
}

func (t *Test[T]) GetParams() map[string]any {
	return t.Params
}

func (t *Test[T]) GetIssueFmtFunc() IssueFmtFunc {
	return t.IssueFmtFunc
}

func (t *Test[T]) SetIssueCode(code zconst.ZogIssueCode) {
	t.IssueCode = code
}

func (t *Test[T]) SetIssuePath(path string) {
	t.IssuePath = path
}

func (t *Test[T]) SetParams(params map[string]any) {
	t.Params = params
}

func (t *Test[T]) SetIssueFmtFunc(fmter IssueFmtFunc) {
	t.IssueFmtFunc = fmter
}

// returns a required test to be used for processor.Required() method
func Required[T any]() Test[T] {
	t := Test[T]{
		IssueCode: zconst.IssueCodeRequired,
		// this is not an accident. required is only a test because it makes it easier to handle error messages. But the function to check if the value is a zero value is out of the scope of this test.
		Func: nil,
	}
	return t
}

type LengthCapable[K any] interface {
	~[]any | ~[]K | ~string | map[any]any | ~chan any
}

func LenMin[T LengthCapable[any]](n int) (Test[*T], BoolTFunc[*T]) {
	fn := func(val *T, ctx Ctx) bool {
		return len(*val) >= n
	}

	t := Test[*T]{
		IssueCode: zconst.IssueCodeMin,
		Params:    make(map[string]any, 1),
	}
	t.Params[zconst.IssueCodeMin] = n
	return t, fn
}

func LenMax[T LengthCapable[any]](n int) (Test[*T], BoolTFunc[*T]) {
	fn := func(val *T, ctx Ctx) bool {
		return len(*val) <= n
	}

	t := Test[*T]{
		IssueCode: zconst.IssueCodeMax,
		Params:    make(map[string]any, 1),
	}
	t.Params[zconst.IssueCodeMax] = n
	return t, fn
}

func Len[T LengthCapable[any]](n int) (Test[*T], BoolTFunc[*T]) {
	fn := func(val *T, ctx Ctx) bool {
		return len(*val) == n
	}

	t := Test[*T]{
		IssueCode: zconst.IssueCodeLen,
		Params:    make(map[string]any, 1),
	}
	t.Params[zconst.IssueCodeLen] = n
	return t, fn
}

func In[T any](values []T) (Test[*T], BoolTFunc[*T]) {
	fn := func(val *T, ctx Ctx) bool {
		for _, value := range values {
			if reflect.DeepEqual(*val, value) {
				return true
			}
		}
		return false
	}

	t := Test[*T]{
		IssueCode: zconst.IssueCodeOneOf,
		Params:    make(map[string]any, 1),
	}
	t.Params[zconst.IssueCodeOneOf] = values
	return t, fn
}

func EQ[T comparable](n T) (Test[*T], BoolTFunc[*T]) {
	fn := func(val *T, ctx Ctx) bool {
		return *val == n
	}

	t := Test[*T]{
		IssueCode: zconst.IssueCodeEQ,
		Params:    make(map[string]any, 1),
	}
	t.Params[zconst.IssueCodeEQ] = n
	return t, fn
}

func LTE[T constraints.Ordered](n T) (Test[*T], BoolTFunc[*T]) {
	fn := func(val *T, ctx Ctx) bool {
		return *val <= n
	}

	t := Test[*T]{
		IssueCode: zconst.IssueCodeLTE,
		Params:    make(map[string]any, 1),
	}
	t.Params[zconst.IssueCodeLTE] = n
	return t, fn
}

func GTE[T constraints.Ordered](n T) (Test[*T], BoolTFunc[*T]) {
	fn := func(val *T, ctx Ctx) bool {
		return *val >= n
	}

	t := Test[*T]{
		IssueCode: zconst.IssueCodeGTE,
		Params:    make(map[string]any, 1),
	}
	t.Params[zconst.IssueCodeGTE] = n
	return t, fn
}

func LT[T constraints.Ordered](n T) (Test[*T], BoolTFunc[*T]) {
	fn := func(val *T, ctx Ctx) bool {
		return *val < n
	}

	t := Test[*T]{
		IssueCode: zconst.IssueCodeLT,
		Params:    make(map[string]any, 1),
	}
	t.Params[zconst.IssueCodeLT] = n
	return t, fn
}

func GT[T constraints.Ordered](n T) (Test[*T], BoolTFunc[*T]) {
	fn := func(val *T, ctx Ctx) bool {
		return *val > n
	}

	t := Test[*T]{
		IssueCode: zconst.IssueCodeGT,
		Params:    make(map[string]any, 1),
	}
	t.Params[zconst.IssueCodeGT] = n
	return t, fn
}
